

package com.smallrig.mall.template.config;

import cn.hutool.core.collection.CollectionUtil;
import com.google.common.base.Preconditions;
import com.google.common.collect.BoundType;
import com.google.common.collect.Range;
import com.smallrig.mall.template.auto.AutoCreateTable;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.driver.jdbc.core.datasource.ShardingSphereDataSource;
import org.apache.shardingsphere.infra.config.exception.ShardingSphereConfigurationException;
import org.apache.shardingsphere.sharding.api.sharding.ShardingAutoTableAlgorithm;
import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Year;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Interval sharding algorithm.
 */
@Getter
@Slf4j
public final class MyIntervalShardingAlgorithm implements StandardShardingAlgorithm<Comparable<?>> ,ShardingAutoTableAlgorithm, AutoCreateTable {

    private static final String DATE_TIME_PATTERN_KEY = "datetime-pattern";

    private static final String DATE_TIME_LOWER_KEY = "datetime-lower";
    
    private static final String DATE_TIME_UPPER_KEY = "datetime-upper";

    private static final String SHARDING_SUFFIX_FORMAT_KEY = "sharding-suffix-pattern";
    
    private static final String INTERVAL_AMOUNT_KEY = "datetime-interval-amount";
    
    private static final String INTERVAL_UNIT_KEY = "datetime-interval-unit";


    @Getter
    @Setter
    private Properties props = new Properties();
    
    private DateTimeFormatter dateTimeFormatter;
    
    private LocalDateTime dateTimeLower;
    
    private LocalDateTime dateTimeUpper;

    @Getter
    private DateTimeFormatter tableSuffixPattern;

    @Getter
    private int stepAmount;

    @Getter
    private ChronoUnit stepUnit;

    private  LocalDateTime getLastDay(){
//        int year = LocalDateTime.now().getYear();
//        return LocalDateTime.of(year,12,31,23,59,59);
        return LocalDateTime.now();
    }

    @Override
    public void init() {
        String dateTimePattern = getDateTimePattern();
        dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimePattern);

        dateTimeLower = getDateTimeLower();
//        dateTimeUpper = getDateTimeUpper();
        dateTimeUpper = getLastDay();
        tableSuffixPattern = getTableSuffixPattern();
        stepAmount = Integer.parseInt(props.getOrDefault(INTERVAL_AMOUNT_KEY, 1).toString());
        stepUnit = props.containsKey(INTERVAL_UNIT_KEY) ? getStepUnit(props.getProperty(INTERVAL_UNIT_KEY)) : ChronoUnit.DAYS;
    }

    private String getDateTimePattern() {
        Preconditions.checkArgument(props.containsKey(DATE_TIME_PATTERN_KEY), "%s can not be null.", DATE_TIME_PATTERN_KEY);
        return props.getProperty(DATE_TIME_PATTERN_KEY);
    }


    private LocalDateTime getDateTimeLower() {
        Preconditions.checkArgument(props.containsKey(DATE_TIME_LOWER_KEY), "%s can not be null.", DATE_TIME_LOWER_KEY);
        return getDateTime(DATE_TIME_LOWER_KEY, props.getProperty(DATE_TIME_LOWER_KEY));
    }
    
    private LocalDateTime getDateTimeUpper() {
        return props.containsKey(DATE_TIME_UPPER_KEY) ? getDateTime(DATE_TIME_UPPER_KEY, props.getProperty(DATE_TIME_UPPER_KEY)) : LocalDateTime.now();
    }
    
    private LocalDateTime getDateTime(final String dateTimeKey, final String dateTimeValue) {
        try {
            return LocalDateTime.parse(dateTimeValue, dateTimeFormatter);
        } catch (final DateTimeParseException ex) {
            throw new ShardingSphereConfigurationException("Invalid %s, datetime pattern should be 2022-11-11T23:59:59.000, value is `%s`", dateTimeKey, dateTimeValue);
        }
    }
    
    private DateTimeFormatter getTableSuffixPattern() {
        Preconditions.checkArgument(props.containsKey(SHARDING_SUFFIX_FORMAT_KEY), "%s can not be null.", SHARDING_SUFFIX_FORMAT_KEY);
        return DateTimeFormatter.ofPattern(props.getProperty(SHARDING_SUFFIX_FORMAT_KEY));
    }
    
    private ChronoUnit getStepUnit(final String stepUnit) {
        for (ChronoUnit each : ChronoUnit.values()) {
            if (each.toString().equalsIgnoreCase(stepUnit)) {
                return each;
            }
        }
        throw new UnsupportedOperationException(String.format("Cannot find step unit for specified %s property: `%s`", INTERVAL_UNIT_KEY, stepUnit));
    }
    
    @Override
    public String doSharding(final Collection<String> availableTargetNames, final PreciseShardingValue<Comparable<?>> shardingValue) {
        return doSharding(availableTargetNames, Range.singleton(shardingValue.getValue())).stream().findFirst().orElse(null);
    }
    
    @Override
    public Collection<String> doSharding(final Collection<String> availableTargetNames, final RangeShardingValue<Comparable<?>> shardingValue) {
        return doSharding(availableTargetNames, shardingValue.getValueRange());
    }
    
    private Collection<String> doSharding(final Collection<String> availableTargetNames, final Range<Comparable<?>> range) {
        dateTimeUpper = getLastDay();
        Set<String> result = new HashSet<>();
        LocalDateTime calculateTime = dateTimeLower;
        while (!calculateTime.isAfter(dateTimeUpper)) {
            if (hasIntersection(Range.closedOpen(calculateTime, calculateTime.plus(stepAmount, stepUnit)), range)) {
                result.addAll(getMatchedTables(calculateTime, availableTargetNames));
            }
            calculateTime = calculateTime.plus(stepAmount, stepUnit);
        }

        return result;
    }


    private boolean hasIntersection(final Range<LocalDateTime> calculateRange, final Range<Comparable<?>> range) {
        LocalDateTime lower = range.hasLowerBound() ? parseDateTime(range.lowerEndpoint().toString()) : dateTimeLower;
        LocalDateTime upper = range.hasUpperBound() ? parseDateTime(range.upperEndpoint().toString()) : dateTimeUpper;
        BoundType lowerBoundType = range.hasLowerBound() ? range.lowerBoundType() : BoundType.CLOSED;
        BoundType upperBoundType = range.hasUpperBound() ? range.upperBoundType() : BoundType.CLOSED;
        Range<LocalDateTime> dateTimeRange = Range.range(lower, lowerBoundType, upper, upperBoundType);
        return calculateRange.isConnected(dateTimeRange) && !calculateRange.intersection(dateTimeRange).isEmpty();
    }
    
    private LocalDateTime parseDateTime(final String value) {
        return LocalDateTime.parse(value, dateTimeFormatter);
    }
    
    private Collection<String> getMatchedTables(final LocalDateTime dateTime, final Collection<String> availableTargetNames) {
        String tableSuffix = dateTime.format(tableSuffixPattern);
        return availableTargetNames.parallelStream().filter(each -> each.endsWith(tableSuffix)).collect(Collectors.toSet());
    }
    
    @Override
    public String getType() {
        return "MY_INTERVAL";
    }


    @Override
    public Collection<String> getAllPropertyKeys() {
        return Arrays.asList(DATE_TIME_LOWER_KEY, DATE_TIME_UPPER_KEY, SHARDING_SUFFIX_FORMAT_KEY,INTERVAL_AMOUNT_KEY,INTERVAL_UNIT_KEY);
    }

    @Override
    public int getAutoTablesAmount() {
        return 0;
    }


    /**
     *
     * @param oldActualDataNodes ds1.t_order_base_201912,ds1.t_order_base_202001
     * @return
     */
    @Override
    public String buildNodes(String oldActualDataNodes) {
        String[] split = oldActualDataNodes.split(",");
        String last = split[split.length - 1];
        int idx = last.lastIndexOf("_");
        String table = last.substring(0, idx);
        String month = last.substring(idx+1);

        List<String> months = null;
        if(stepUnit.equals(ChronoUnit.YEARS)){
            months = yearList(month);
        }else if(stepUnit.equals(ChronoUnit.MONTHS)){
            months = monthList(month);
        }

        StringBuilder sb = new StringBuilder();
        sb.append(oldActualDataNodes).append(",");

        for(int i=0;i<months.size();i++){
            sb.append(table).append("_").append(months.get(i));
            if(i!=months.size()-1){
                sb.append(",");
            }
        }
        return sb.toString();
    }

    /**
     *
     * @param month 202001
     * @return 202007,202101,202107,202201,202207。。。。
     */
    private List<String> monthList(String month){
        YearMonth nextMonth = YearMonth.now().plusMonths(1);
//        YearMonth nextMonth = YearMonth.now().withYear(2022).withMonth(12).plusMonths(1);

        YearMonth curMonth = YearMonth.parse(month, tableSuffixPattern).plus(stepAmount,stepUnit);

        List<String> months = new ArrayList<>();
        while(curMonth.compareTo(nextMonth)<=0){
            months.add(curMonth.format(tableSuffixPattern));
            curMonth = curMonth.plus(stepAmount,stepUnit);
        }
        return months;
    }

    private List<String> yearList(String year){
        Year nextYear = Year.now().plusYears(1);

        Year curYear = Year.parse(year, tableSuffixPattern).plus(stepAmount,stepUnit);

        List<String> months = new ArrayList<>();
        while(curYear.compareTo(nextYear)<=0){
            months.add(curYear.format(tableSuffixPattern));
            curYear = curYear.plus(stepAmount,stepUnit);
        }
        return months;

    }


    /**
     * 一个月分一次表 yyyyMM ds1.my_interval_${2020..2021}${['01','02','03','04','05','06','07','08','09','10','11','12']}
     * 三个月分一次表 yyyyMM ds1.my_interval_${2020..2021}${['01','04','07','10']}
     * 六个月分一次表 yyyyMM ds1.my_interval_${2020..2021}0${[1,7]}
     * 一年分一次表 yyyy ds1.my_interval_2020 只写初始年份即可
     *第一次新建表的时候会走随机:org.apache.shardingsphere.singletable.route.SingleTableRouteEngine#getRandomRouteUnit(org.apache.shardingsphere.singletable.rule.SingleTableRule)
     * 每个月执行定时任务
     *ShardingRouteEngineFactory      org.apache.shardingsphere.singletable.route.SingleTableRouteEngine  org.apache.shardingsphere.singletable.route.SingleTableRouteEngine#fillRouteContext(org.apache.shardingsphere.singletable.rule.SingleTableRule, org.apache.shardingsphere.infra.route.context.RouteContext, java.util.Collection)
     * org.apache.shardingsphere.infra.route.engine.impl.PartialSQLRouteExecutor#route(org.apache.shardingsphere.infra.binder.LogicSQL, org.apache.shardingsphere.infra.metadata.ShardingSphereMetaData)
     * @param dataSource 数据源
     * @param logicTable 逻辑表名
     * @throws SQLException
     */
    @Override
    public void createTables(ShardingSphereDataSource dataSource, String logicTable) throws SQLException {

        try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) {

            //每个月第一天建下张表
            LocalDate now = null;
            if(stepUnit.equals(ChronoUnit.YEARS)){
                now = LocalDate.now().plusYears(1);
            }else if(stepUnit.equals(ChronoUnit.MONTHS)){
                now = LocalDate.now().plusMonths(1);
            }

            LocalDate lastTime = now.minus(stepAmount, stepUnit);
            String oldTable = getTableName(logicTable,lastTime);
            String newTable = getTableName(logicTable,now);
            if(!oldTable.equals(newTable)){
                String sql = "create table IF NOT EXISTS `" + newTable + "` like  `" + oldTable + "`";
                boolean f = statement.execute(sql);
                log.info("sql={},执行结果={}",sql,f);
            }
        }
        catch (SQLException e) {
            throw e;
        }
    }


    private String getTableName(String logicTable,LocalDate time){
        if(stepUnit.equals(ChronoUnit.MONTHS)){

            List<LocalDate> timeList = new ArrayList<>();

            for(int i=1;i<=12;i+=stepAmount){
                LocalDate yearMonth = LocalDate.now()
                        .withYear(time.getYear())
                        .withMonth(i)
                        .withDayOfMonth(1);

                timeList.add(0,yearMonth);
            }

            for (LocalDate dateTime : timeList) {
                if(time.compareTo(dateTime)>=0){
                    return logicTable+"_" + dateTime.format(tableSuffixPattern);
                }
            }

        }else if(stepUnit.equals(ChronoUnit.YEARS)){

            int initYear = dateTimeLower.getYear();

            int a = (time.getYear() - initYear) / stepAmount;

            return logicTable +"_" + (initYear+a*stepAmount);
        }
        return null;
    }


}
